<?php

namespace Aws\S3;

use Aws\Api\Parser\AbstractParser;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\CommandInterface;
use Aws\S3\Exception\S3Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;

/**
 * @internal Decorates a parser for the S3 service to validate the response checksum.
 */
class ValidateResponseChecksumParser extends AbstractParser {
	use CalculatesChecksumTrait;

	/**
	 * @param callable $parser Parser to wrap.
	 */
	public function __construct(callable $parser, Service $api) {
		$this->api = $api;
		$this->parser = $parser;
	}

	public function __invoke(
		CommandInterface  $command,
		ResponseInterface $response
	) {
		$fn = $this->parser;
		$result = $fn($command, $response);

		//Skip this middleware if the operation doesn't have an httpChecksum
		$op = $this->api->getOperation($command->getName());
		$checksumInfo = isset($op['httpChecksum'])
			? $op['httpChecksum']
			: [];
		if(empty($checksumInfo)) {
			return $result;
		}

		//Skip this middleware if the operation doesn't send back a checksum, or the user doesn't opt in
		$checksumModeEnabledMember = isset($checksumInfo['requestValidationModeMember'])
			? $checksumInfo['requestValidationModeMember']
			: "";
		$checksumModeEnabled = isset($command[$checksumModeEnabledMember])
			? $command[$checksumModeEnabledMember]
			: "";
		$responseAlgorithms = isset($checksumInfo['responseAlgorithms'])
			? $checksumInfo['responseAlgorithms']
			: [];
		if(empty($responseAlgorithms)
			|| strtolower($checksumModeEnabled) !== "enabled"
		) {
			return $result;
		}

		if(extension_loaded('awscrt')) {
			$checksumPriority = ['CRC32C', 'CRC32', 'SHA1', 'SHA256'];
		} else {
			$checksumPriority = ['CRC32', 'SHA1', 'SHA256'];
		}
		$checksumsToCheck = array_intersect($responseAlgorithms, $checksumPriority);
		$checksumValidationInfo = $this->validateChecksum($checksumsToCheck, $response);

		if($checksumValidationInfo['status'] == "SUCCEEDED") {
			$result['ChecksumValidated'] = $checksumValidationInfo['checksum'];
		} else if($checksumValidationInfo['status'] == "FAILED") {
			//Ignore failed validations on GetObject if it's a multipart get which returned a full multipart object
			if($command->getName() == "GetObject"
				&& !empty($checksumValidationInfo['checksumHeaderValue'])
			) {
				$headerValue = $checksumValidationInfo['checksumHeaderValue'];
				$lastDashPos = strrpos($headerValue, '-');
				$endOfChecksum = substr($headerValue, $lastDashPos + 1);
				if(is_numeric($endOfChecksum)
					&& intval($endOfChecksum) > 1
					&& intval($endOfChecksum) < 10000) {
					return $result;
				}
			}
			throw new S3Exception(
				"Calculated response checksum did not match the expected value",
				$command
			);
		}
		return $result;
	}

	public function parseMemberFromStream(
		StreamInterface $stream,
		StructureShape  $member,
		                $response
	) {
		return $this->parser->parseMemberFromStream($stream, $member, $response);
	}

	/**
	 * @param $checksumPriority
	 * @param ResponseInterface $response
	 */
	public function validateChecksum($checksumPriority, ResponseInterface $response) {
		$checksumToValidate = $this->chooseChecksumHeaderToValidate(
			$checksumPriority,
			$response
		);
		$validationStatus = "SKIPPED";
		$checksumHeaderValue = null;
		if(!empty($checksumToValidate)) {
			$checksumHeaderValue = $response->getHeader(
				'x-amz-checksum-'.$checksumToValidate
			);
			if(isset($checksumHeaderValue)) {
				$checksumHeaderValue = $checksumHeaderValue[0];
				$calculatedChecksumValue = $this->getEncodedValue(
					$checksumToValidate,
					$response->getBody()
				);
				$validationStatus = $checksumHeaderValue == $calculatedChecksumValue
					? "SUCCEEDED"
					: "FAILED";
			}
		}
		return [
			"status" => $validationStatus,
			"checksum" => $checksumToValidate,
			"checksumHeaderValue" => $checksumHeaderValue,
		];
	}

	/**
	 * @param $checksumPriority
	 * @param ResponseInterface $response
	 */
	public function chooseChecksumHeaderToValidate(
		$checksumPriority,
		ResponseInterface $response
	) {
		foreach($checksumPriority as $checksum) {
			$checksumHeader = 'x-amz-checksum-'.$checksum;
			if($response->hasHeader($checksumHeader)) {
				return $checksum;
			}
		}
		return null;
	}
}
